package util;

import java.io.*;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * @author cramakrishnan
 *
 * Copyright (C) 2003, C. Ramakrishnan / Auracle
 * All rights reserved.
 * 
 * See license.txt (or license.rtf) for license information.
 */
public class Logger implements Runnable {
	// log levels
	public final static int ERROR = 0;
	public final static int WARNING = 1;
	public final static int DEBUG = 2;
	public final static int INFO = 3;

	// modules
	public final static int SERIAL_PORT = 1;
	public final static int FLOCK = 1 << 1;
	public final static int BIRD = 1 << 2;	
	public final static int HARDWARE = 1 << 3;	
	public final static int STREAM = 1 << 4;	
	public final static int GUI = 1 << 5;
	public final static int SYSTEM = 1 << 6;
	public final static int ALL = 0xFF;

	// is logging on?
	static boolean isLogging = true;

	// what level of logging am I doing?
	protected int currentLogLevel = 3;
	// which modules am I logging
	protected int currentLogModules = 0xFF;
	
	protected int excludeLogModules = 0;

	// singleton
	protected static Logger currentLogger;

	// File stuff
	protected String logFileName;
	protected FileOutputStream logFileStream;
	protected PrintStream logStream;

	// writer thread
	protected Thread fileWriterThread = null;
	protected MessageQueueItem[] messageQueue;
	protected int messageQueueMaxSize = 15;
	protected int writeIndex = 0;
	protected int readIndex = 0;
	protected boolean keepRunning;
	

	// Private class for tracking information
	protected class MessageQueueItem {
		int logLevel;
		int module;
		Throwable e;
		String text;
		MessageQueueItem(int logLevel, int module, Throwable e, String text) {
			this.logLevel = logLevel;
			this.module = module;
			this.text = text;
			this.e = e;
		}
	}

	/**
	 * @return the current log modules
	 */
	public int getCurrentLogModules() {
		return currentLogModules;
	}

	/**
	 * @param currentLogModules
	 */
	public void setCurrentLogModules(int currentLogModules) {
		this.currentLogModules = currentLogModules;
	}
	
	public int getExcludeLogModules(){
		return excludeLogModules;
	}
	
	public void setExcludeLogModules(int excludeLogModules){
		this.excludeLogModules = excludeLogModules;
	}

	/**
	 * @return the current log level
	 */
	public int getCurrentLogLevel() {
		return currentLogLevel;
	}

	/**
	 * @param currentLogLevel
	 */
	public void setCurrentLogLevel(int currentLogLevel) {
		this.currentLogLevel = currentLogLevel;
	}

	protected Logger() {
		// default to the console
		logStream = System.out;
		messageQueue = new MessageQueueItem[messageQueueMaxSize];
	}

	/**
	 * Find and return the singleton instance of the logger
	 * @return The currently active logger
	 */
	public static Logger getCurrentLogger() {
		if (null == currentLogger)
			currentLogger = new Logger();
		return currentLogger;
	}

	/**
	 * Log a message to where ever we are currently logging things
	 * @param logLevel  The severity of this message
	 * @param module    The module that generated of this message
	 * @param message   The text to log
	 */
	public void log(int logLevel, int module, String message) {
		MessageQueueItem item = createMessage(logLevel, module, message, null);
		if (null == item)
			return;
		synchronized (this) {
			// write this out if we need to, o.w. let the file writer thread take care of it
			if (isNotLoggingInSeparateThread())
				logItem(item);
			else {
				messageQueue[writeIndex] = item;
				if (++writeIndex >= messageQueueMaxSize) {
					writeIndex = 0;
				}
				notify();
			}
		}
	}

	/**
	 * Same as the other log, but include info about an exception
	 * @param logLevel  The severity of this message
	 * @param module    The module that generated of this message
	 * @param message   The text to log
	 */
	public void log(int logLevel, int module, String message, Throwable e) {
		MessageQueueItem item = createMessage(logLevel, module, message, e);
		if (null == item)
			return;
		synchronized (this) {
			// write this out if we need to, o.w. let the file writer thread take care of it
			if (isNotLoggingInSeparateThread()) {
				logItem(item);
			} else {
				messageQueue[writeIndex] = item;
				if (++writeIndex >= messageQueueMaxSize) {
					writeIndex = 0;
				}
				notify();
			}
		}
	}
	
	protected void logItem(MessageQueueItem item) {
		logStream.println(item.text);
		if (item.e != null) {
			item.e.printStackTrace(logStream);
		}
	}

	private boolean isNotLoggingInSeparateThread() {
		// return null == fileWriterThread;
		// if keep running is false, then we are logging in the same thread as
		// the original
		return !keepRunning;
	}

	/**
	 * Set the logging output to go to the console (System.out)
	 * 
	 */
	public void logToConsole() {
		logStream = System.out;
		if (null != logFileStream) {
			try {
				logFileStream.close();
			} catch (Exception e) {
				e.printStackTrace(logStream);
			}
		}
	}

	public void logToFile(String fileName) throws IOException {
		if (null != logFileStream)
			logFileStream.close();
		logFileName = fileName;
		logFileStream = new FileOutputStream(logFileName);
		logStream = new PrintStream(logFileStream);
	}

	private void appendTimestamp(StringBuffer sb) {

		if (false) {
			// print date in ISO format
			GregorianCalendar timestamp = new GregorianCalendar();
			sb.append(timestamp.get(GregorianCalendar.YEAR));
			sb.append(".");
			sb.append(timestamp.get(GregorianCalendar.MONTH));
			sb.append(".");
			sb.append(timestamp.get(GregorianCalendar.DATE));
			sb.append(" ");
			sb.append(timestamp.get(GregorianCalendar.HOUR));
			sb.append(":");
			sb.append(timestamp.get(GregorianCalendar.MINUTE));
			sb.append(":");
			sb.append(timestamp.get(GregorianCalendar.SECOND));
		} else {
			Date timestamp = new Date();
			sb.append(timestamp);
		}
	}

	private void appendLevel(int logLevel, StringBuffer sb) {
		switch (logLevel) {
			case ERROR :
				sb.append("ERROR  ");
				break;
			case WARNING :
				sb.append("WARNING");
				break;
			case DEBUG :
				sb.append("DEBUG  ");
				break;
			case INFO :
				sb.append("INFO   ");
				break;
		}
	}

	private void appendModule(int module, StringBuffer sb) {
		// I'm assuming people don't bit xor together modules when they log
		// only for configuring which modules the logger is listening to.
		switch (module) {
			case SERIAL_PORT :
				sb.append("SERIAL PORT");
				break;
			case FLOCK :
				sb.append("   FLOCK   ");
				break;
			case BIRD :
				sb.append("    BIRD   ");
				break;
			case HARDWARE :
				sb.append("  HARDWARE ");
				break;	
			case STREAM :
				sb.append("   STREAM  ");
				break;	
			case GUI :
				sb.append("     GUI   ");
				break;
			case SYSTEM :
				sb.append("   SYSTEM  ");
				break;
			case ALL :
				sb.append("    ALL    ");
				break;
		}
	}

	private MessageQueueItem createMessage(
		int logLevel,
		int module,
		String message,
		Throwable e) {

		// do we really want to log this message?
		if (logLevel > currentLogLevel)
			return null;
		if ((currentLogModules & module) == 0)
			return null;
		if(excludeLogModules == module)
			return null;
		StringBuffer sb = new StringBuffer();
		sb.append("FOB ");
		appendLevel(logLevel, sb);
		sb.append(" ");
		appendTimestamp(sb);
		sb.append(" -- ");
		appendModule(module, sb);
		sb.append(" -- ");
		sb.append(message);
		return new MessageQueueItem(logLevel, module, e, sb.toString());
	}

	public void finalize() {
		close();
	}

	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		Thread.currentThread().setName("Main Logger Thread");
		log(INFO, SYSTEM, "Logging thread started");
		keepRunning = true;
		while (keepRunning) {
			synchronized (this) {
				try {
					wait();
				} catch (InterruptedException e) {
					// ok, keep going
				}
				int numMessagesToProcess = writeIndex - readIndex;
				if (numMessagesToProcess < 0) {
					numMessagesToProcess =
						messageQueueMaxSize + numMessagesToProcess;
				}
				for (; numMessagesToProcess > 0; numMessagesToProcess--) {
					MessageQueueItem item = messageQueue[readIndex];
					logItem(item);
					if (++readIndex >= messageQueueMaxSize)
						readIndex = 0;
				}
			}
		}
		fileWriterThread = null;
		log(INFO, SYSTEM, "Logging thread stopped");
	}

	public void start() {
		fileWriterThread = new Thread(this);
		keepRunning = true;
		fileWriterThread.setName("Logger: File Writer Thread");
		fileWriterThread.start();
	}

	public void stop() {
		keepRunning = false;
		// Wake up the thread so that it knows it needs to go down
		synchronized (this) {
			notify();
		}
	}

	/**
	 * Close the logger's stream
	 */
	public void close() {
		logStream.println(
			"----------------- FOB Logger Stream Closed -----------------");
		try {
			if (null != logFileStream)
				logFileStream.close();
		} catch (Exception e) {
			// do nothing
		}
	}
}
